import { Tabs, Callout } from "nextra/components";
import UniversalTabs from "../../../../components/UniversalTabs";

# Sticky Worker Assignment (Beta)

<Callout type="info">
  This feature is currently in beta and may be subject to change.
</Callout>

Sticky assignment is a workflow property that allows you to specify that all steps of a workflow should be assigned to the same worker for the duration of its execution. This can be useful in situations like when you need to maintain expensive local memory state across multiple steps in a workflow or ensure that certain workflows are processed by the same worker for consistency.

<Callout type="warn">
  This feature is only compatible with long lived workers, and not webhook
  workers.
</Callout>

## Setting Sticky Assignment

Sticky assignment is set on the workflow level by adding the `sticky` property to the workflow definition. When a workflow is marked as sticky, all steps within that workflow will be assigned to the same worker for the duration of the workflow execution.

<Callout type="warning">
  While sticky assignment can be useful in certain scenarios, it can also
  introduce potential bottlenecks if the assigned worker becomes unavailable, or
  if local state is not maintained when the job is picked up. Be sure to
  consider the implications of sticky assignment when designing your workflows
  and have a plan in place to handle local state issues.
</Callout>

There are two strategies for setting sticky assignment:

- `SOFT`: The all steps in the workflow will attempt to be assigned to the same worker, but if that worker is unavailable, it will be assigned to another worker.
- `HARD`: The all steps in the workflow will only be assigned to the same worker. If that worker is unavailable, the step run will not be assigned to another worker and will remain in a pending state until the original worker becomes available or timeout is reached. (See [Scheduling Timeouts](../timeouts.mdx))

<UniversalTabs items={['Python', 'Typescript', 'Go']}>
  <Tabs.Tab>

```python
@hatchet.workflow(
    on_events=["user:create"],
    sticky=StickyStrategy.SOFT, # <-- specify the sticky strategy
)
class StickyWorkflow:
    @hatchet.step()
    def step1a(self, context: Context):
        return {"worker": context.worker.id()}

    @hatchet.step()
    def step1b(self, context: Context):
        return {"worker": context.worker.id()}

    @hatchet.step(parents=["step1a", "step1b"])
    def step2(self, context: Context):
        return {"worker": context.worker.id()}

```

  </Tabs.Tab>
  <Tabs.Tab>

```typescript
const myWorkflow: Workflow = {
  id: "my-workflow",
  description: "A workflow triggered by an event",
  sticky: StickyStrategy.SOFT, // <-- specify the sticky strategy
  on: {
    event: "user:created",
  },
  steps: [
    // Define your workflow steps here
  ],
};
```

  </Tabs.Tab>
  <Tabs.Tab>

```go
	err = w.RegisterWorkflow(
		&worker.WorkflowJob{
			On:             worker.Events("user:create:sticky"),
			Name:           "sticky",
			Description:    "sticky",
			StickyStrategy: types.StickyStrategyPtr(types.StickyStrategy_SOFT),
			Steps: []*worker.WorkflowStep{
				worker.Fn(func(ctx worker.HatchetContext) (result *stepOneOutput, err error) {
					return &stepOneOutput{
						Message: ctx.Worker().ID(),
					}, nil
				}).SetName("step-one"),
				worker.Fn(func(ctx worker.HatchetContext) (result *stepOneOutput, err error) {
					return &stepOneOutput{
						Message: ctx.Worker().ID(),
					}, nil
				}).SetName("step-two"),
				worker.Fn(func(ctx worker.HatchetContext) (result *stepOneOutput, err error) {
					return &stepOneOutput{
						Message: ctx.Worker().ID(),
					}, nil
				}).SetName("step-three").AddParents("step-one", "step-two"),
			},
		},
	)
```

  </Tabs.Tab>
</UniversalTabs>

In this example, the `sticky` property is set to `SOFT`, which means that the workflow will attempt to be assigned to the same worker for the duration of its execution. If the original worker is unavailable, the workflow will be assigned to another worker.

## Sticky Child Workflows

It is possible to spawn child workflows on the same worker as the parent workflow by setting the `sticky` property to `true` in the `spawnWorkflow` method options. This can be useful when you need to maintain local state across multiple workflows or ensure that child workflows are processed by the same worker for consistency.

However, the child workflow must:

1. Specify a `sticky` strategy in the child workflow's definition
2. Be registered with the same worker as the parent workflow

If either condition is not met, an error will be thrown when the child workflow is spawned.

<UniversalTabs items={['Python', 'Typescript', 'Go']}>
  <Tabs.Tab>

```python
@hatchet.workflow(
    on_events=["sticky:parent"],
)
class StickyWorkflow:
    @hatchet.step(parents=["step1a", "step1b"])
    async def parent_step(self, context: Context):
        ref = context.spawn_workflow('StickyChildWorkflow', {}, options={"sticky": True})
        await ref.result()
        return {"worker": context.worker.id()}
```

```python
@hatchet.workflow(
on_events=["sticky:child"],
sticky=StickyStrategy.SOFT
)
class StickyChildWorkflow:
@hatchet.step()
def child(self, context: Context):
return {"worker": context.worker.id()}

```

  </Tabs.Tab>
  <Tabs.Tab>

```typescript
const parentWorkflow: Workflow = {
  id: "parent-sticky-workflow",
  description: "test",
  steps: [
    {
      name: "step1",
      run: async (ctx) => {
        const results: Promise<any>[] = [];

        for (let i = 0; i < 50; i++) {
          const result = await ctx.spawnWorkflow(
            childWorkflow,
            {},
            { sticky: true },
          );
          results.push(result.result());
        }

        console.log("Results:", await Promise.all(results));

        return { step1: "step1 results!" };
      },
    },
  ],
};
```

  </Tabs.Tab>
  <Tabs.Tab>

```go
	err = w.RegisterWorkflow(
		&worker.WorkflowJob{
			On:             worker.Events("user:create:sticky"),
			Name:           "sticky",
			Description:    "sticky",
			Steps: []*worker.WorkflowStep{
				worker.Fn(func(ctx worker.HatchetContext) (result *stepOneOutput, err error) {

    				sticky := true

    				_, err = ctx.SpawnWorkflow("sticky-child", nil, &worker.SpawnWorkflowOpts{
    					Sticky: &sticky,
    				})

    				if err != nil {
    					return nil, fmt.Errorf("error spawning workflow: %w", err)
    				}

    				return &stepOneOutput{
    					Message: ctx.Worker().ID(),
    				}, nil
    			}).SetName("step-one"),
    		},
    	},
    )

```

  </Tabs.Tab>
</UniversalTabs>
